
import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TestAttemptThread implements Runnable {

	private Integer taskID;
	private Integer attemptID;
	private HashMap<String, String> m;
	private HashMap<String, String> testinnerdirs;
	private static String keys;
	private static String vcPath;
	private static String sandboxPath;
	private static String sandboxKeys;
	private static int deleteFiles;
	private static int simpleTest;
	private static int sandboxNeedWindowsStylePath;
	private static long sleepStep;
	private static long maxSleep;
	private static long compileSleepStep;
	private static long compileMaxSleep;
	private static String pathToAttemptDir;
	private static String pathToAttemptResult;
	private static String testsDir;
	private static String pathToTestingH;

	public static void loadConfig() {
		Properties prop = new Properties();
		try {
			prop.load(new FileInputStream("config.properties"));

			keys = prop.getProperty("keys");
			vcPath = prop.getProperty("vcPath");
			sandboxPath = prop.getProperty("sandboxPath");
			sandboxKeys = prop.getProperty("sandboxKeys");

			deleteFiles = Integer.parseInt(prop.getProperty("deleteFiles"));
			simpleTest = Integer.parseInt(prop.getProperty("simpleTest"));
			if (prop.getProperty("sandboxNeedWindowsStylePath") != null) {
				sandboxNeedWindowsStylePath = Integer.parseInt(prop.getProperty("sandboxNeedWindowsStylePath"));
			}
			sleepStep = Long.parseLong(prop.getProperty("sleepStep"));
			maxSleep = Long.parseLong(prop.getProperty("maxSleep"));

			compileSleepStep = Long.parseLong(prop.getProperty("compileSleepStep"));
			compileMaxSleep = Long.parseLong(prop.getProperty("compileMaxSleep"));

			pathToAttemptDir = prop.getProperty("pathToAttemptDir");
			pathToAttemptResult = prop.getProperty("pathToAttemptResult");

			testsDir = prop.getProperty("testsDir");
			pathToTestingH = prop.getProperty("testingHPath");
		} catch (IOException ex) {
			ex.printStackTrace();
		}
	}
	
	public TestAttemptThread(Integer taskID, Integer attemptID, HashMap<String, String> m) {
		this(taskID, attemptID, m, null);
	}
	
	public TestAttemptThread(Integer taskID, Integer attemptID, HashMap<String, String> m, HashMap<String, String> testdirs) {
		this.taskID = taskID;
		this.attemptID = attemptID;
		this.m = m;
		this.testinnerdirs = testdirs;
	}

	@Override
	public void run() {
		String date = new java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(java.util.Calendar.getInstance().getTime());
		// Create files in special directory
		createFiles(m, date);

		// Compile files into exe-file
		Main.notifyCompileStarted(this.attemptID);
		if (compileFiles(getDirForAttempt(attemptID, date), m.keySet())) {
			Main.notifyCompiled(this.attemptID, true);
			Log.log(this.getClass(), "Attempt " + attemptID + " was compiled", Log.OK);

			// Run tests
			runTests(date);
		} else {
			String compileMessage = StringUtils.implode("\n", FileUtils.readFile(getDirForAttempt(attemptID, date) + "compile.res", "Cp866"));
			Main.notifyCompiled(this.attemptID, compileMessage);
			Log.log(this.getClass(), "Attempt " + attemptID + " wasn't compiled", Log.WARNING);
		}

		// Remove files
		if (deleteFiles == 1) {
			removeFiles(date);
		}
	}

	private boolean runTests(String date) {
		String dir = getDirForAttempt(attemptID, date);
		if (simpleTest == 1) {
			Main.notifyTestStarted(attemptID);
			File output = new File(getPathForResult(attemptID, date));
			output.delete();
			String run = "\"" + dir + "attempt.exe\"";
			run += ">\"" + getPathForResult(attemptID, date) + "\"";
			FileUtils.createFile(getDirForAttempt(attemptID, date) + "test.bat", run);

			try {
				String command = "";
				if (sandboxPath != null && sandboxPath.length() > 0) {
					command = "\"" + sandboxPath + "\" " + sandboxKeys + "  ";
				}
				command += "\"" + getDirForAttempt(attemptID, date) + "test.bat\"";
				if (sandboxNeedWindowsStylePath == 1) {
					command = command.replaceAll("/", "\\\\");
				}
				Process p = Runtime.getRuntime().exec(command);
				//p.waitFor();
				FileUtils.waitForNonEmptyFile(output, sleepStep, maxSleep);
				if (p != null) {
					p.destroy();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			/*
			 * catch (InterruptedException e) { e.printStackTrace(); }
			 */

			if (output.exists()) {
				Log.log(this.getClass(), "Program was tested, attempt " + attemptID, Log.OK);
				Main.notifyTested(attemptID, "single test", "", "", StringUtils.implode("\n", FileUtils.readFile(getPathForResult(attemptID, date))), 1);
				return true;
			} else {
				Log.log(this.getClass(), "Program wasn't tested, attempt " + attemptID, Log.WARNING);
				Main.notifyTested(attemptID, "single test", "", "", "", 0);
				return false;
			}
		}
		else {
			Main.notifyTestStarted(attemptID);
			ArrayList<TestResult> runBlackBoxTests = new ArrayList<>();
			Map<String, ArrayList<RemoteAutotesterTest>> testsGroups = new HashMap<>();
			int totalTestsCount = 0;
			// Найти все тесты для программы
			for (String testinnerdir : testinnerdirs.values()) {
				ArrayList<RemoteAutotesterTest> testsFromDir = RemoteAutotesterTest.getTestsFromDir(testsDir + testinnerdir);
				testsGroups.put(testinnerdir, testsFromDir);
				totalTestsCount += testsFromDir.size();
			}
			// Уведомить о количестве найденных тестов
			Main.notifyTestfilesFound(attemptID, totalTestsCount);

			if (totalTestsCount > 0) {
				// Провести тестирование
				for (String testinnerdir : testsGroups.keySet()) {
					runBlackBoxTests.addAll(runBlackBoxTests(getDirForAttempt(attemptID, date), testinnerdir, testsGroups.get(testinnerdir)));
				}
				for (TestResult testResult : runBlackBoxTests) {
					Main.notifyTested(
							attemptID,
							testResult.getTest().getTestName(true),
							StringUtils.implode("\n", FileUtils.readFile(testResult.getTest().getTestInputFileFullPath())),
							StringUtils.implode("\n", testResult.getTest().getTestOutput()),
							StringUtils.implode("\n", testResult.getStudentOut()),
							testResult.isSame() ? 1 : 0);
				}
			}
		}
		return false;
	}

	private void removeFiles(String date) {
		try {
			File dir = new File(getDirForAttempt(attemptID, date));
			FileUtils.deleteRecursive(dir);
			if (dir.getParentFile().listFiles().length == 0) {
				dir.getParentFile().delete();
			}
			Log.log(this.getClass(), "Attempt " + attemptID + " files deleted", Log.OK);
		} catch (FileNotFoundException e) {
			Log.log(this.getClass(), "Removing attempt " + attemptID + " files failed", Log.ERROR);
		}
	}

	/**
	 * Компилирует файлы по указанному пути
	 *
	 * @param attemptPath путь, в котором лежат файлы для компиляции
	 * @param names имена компилируемых файлов
	 * @return boolean true, если файл был скомпилирован
	 */
	private boolean compileFiles(String attemptPath, Set<String> names) {
		// Remove files that will be generated
		File runfile = new File(attemptPath + "run.bat");
		runfile.delete();

		File exefile = new File(attemptPath + "attempt.exe");
		exefile.delete();
		try {
			// Create run file
			runfile.createNewFile();
			FileWriter fstream = null;
			fstream = new FileWriter(runfile);
			BufferedWriter out = new BufferedWriter(fstream);
			out.write("cd \"" + attemptPath + "\"\r\n");
			out.write("call \"" + vcPath + "vcvarsall.bat\"\r\n");
			out.write("\"" + vcPath + "bin/cl.exe\"");
			out.write("/Fe\"" + attemptPath + "attempt.exe\" ");
			out.write(keys);
			boolean hasTestingH = false;
			for (String name : names) {
				if (name.indexOf("testing.h") == 0)
					hasTestingH = true;
				out.write(" \"" + attemptPath + name + "\"");
			}
			if (!hasTestingH && pathToTestingH != null) {
				out.write(" \"" + attemptPath + "testing.h\"");
			}
			out.write(">" + attemptPath + "compile.res\r\n");
			out.close();

			Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", attemptPath + "run.bat"});

			if (!FileUtils.waitForNonEmptyFile(exefile, compileSleepStep, compileMaxSleep) && p != null) {
				p.destroy();
				Log.log(this.getClass(), "Too long compile time, attemptid=" + attemptID, Log.WARNING);
			}
		} catch (IOException e) {
		}
		return exefile.exists();
	}

	/**
	 * Тестирование попытки из файла в папке attemptDir по набору тестов tests
	 *
	 * @param attemptDir путь к каталогу с программой
	 * @param tests набор тестов
	 */
	public static ArrayList<TestResult> runBlackBoxTests(String attemptDir, String testinnerdir, ArrayList<RemoteAutotesterTest> tests) {
		ArrayList<TestResult> results = new ArrayList<>();
		File runTestsFile = new File(attemptDir + "runtests.bat");
		File lastResFile = null;
		runTestsFile.delete();
		String commands = "";
		for (RemoteAutotesterTest test : tests) {
			File outFile = new File(attemptDir + test.getTestInputFileName() + ".res");
			outFile.delete();
			commands += attemptDir + "attempt.exe";
			commands += "<" + testsDir + testinnerdir + test.getTestInputFileName() + "";
			commands += ">" + attemptDir + test.getTestInputFileName() + ".res\n";
			lastResFile = new File(attemptDir + test.getTestInputFileName() + ".res");
			lastResFile.delete();
		}
		// Создать bat для запуска всех тестов
		try {
			runTestsFile.createNewFile();
			FileWriter fstream = null;
			try {
				fstream = new FileWriter(runTestsFile);
				BufferedWriter out = new BufferedWriter(fstream);
				try {
					out.write(commands);
					out.close();
				} catch (IOException ex) {
					Logger.getLogger(TestAttemptThread.class.getName()).log(Level.SEVERE, null, ex);
				}
			} catch (IOException ex) {
				Logger.getLogger(TestAttemptThread.class.getName()).log(Level.SEVERE, null, ex);
			}
		} catch (IOException ex) {
			Logger.getLogger(TestAttemptThread.class.getName()).log(Level.SEVERE, null, ex);
		}

		// Запуск тестов
		if (runTestsFile.exists()) {
			String command = "";
			if (sandboxPath != null && sandboxPath.length() > 0) {
				command = "\"" + sandboxPath + "\" " + sandboxKeys + "  ";
			}
			command += "\"" + attemptDir + "runtests.bat\"";
			if (sandboxNeedWindowsStylePath == 1) {
				command = command.replaceAll("/", "\\\\");
			}
			Process p;
			try {
				p = Runtime.getRuntime().exec(command);
				FileUtils.waitForNonEmptyFile(lastResFile, sleepStep, maxSleep);
				if (p != null) {
					p.destroy();
				}
			} catch (IOException ex) {
				Logger.getLogger(TestAttemptThread.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
		// Проверка тестов
		for (RemoteAutotesterTest test : tests) {
			// Открыть файл с результатом
			ArrayList<String> fileContent = new ArrayList<>();
			try {
				FileInputStream fis = new FileInputStream(attemptDir + test.getTestInputFileName() + ".res");
				BufferedReader br = new BufferedReader(new InputStreamReader(fis, Charset.forName("Cp1251")));
				try {
					String line;
					while ((line = br.readLine()) != null) {
						// Считать все данные в массив
						if (line.length() > 0) {
							fileContent.add(line);
						}
					}
					br.close();
					fis.close();
				} catch (IOException ex) {
					Logger.getLogger(RemoteAutotesterTest.class.getName()).log(Level.SEVERE, null, ex);
				}
			} catch (FileNotFoundException ex) {
				Logger.getLogger(RemoteAutotesterTest.class.getName()).log(Level.SEVERE, null, ex);
			}
			Patch diff = DiffUtils.diff(fileContent, test.getTestOutput());
			TestResult testResult = new TestResult(test, fileContent);
			results.add(testResult);
		}
		return results;
	}

	/**
	 * Get path for saving src files.
	 *
	 * @param attemptID
	 * @param date
	 * @return
	 */
	private static String getDirForAttempt(Integer attemptID, String date) {		
		return pathToAttemptDir.replaceAll("#ATTEMPTID#", String.format("%06d", attemptID)).replaceAll("#DATE#", date);
	}

	private static String getPathForResult(Integer attemptID, String date) {
		return pathToAttemptResult.replaceAll("#ATTEMPTID#", String.format("%06d", attemptID)).replaceAll("#DATE#", date);
	}

	/**
	 * Создание файлов попытки.
	 *
	 * @param m список файлов попытки.
	 */
	private void createFiles(HashMap<String, String> m, String date) {
		String attemptPath = getDirForAttempt(attemptID, date);
		File attemptDir = new File(attemptPath);
		attemptDir.mkdirs();
		for (String name : m.keySet()) {
			if (name.indexOf("testing.h") == 0)
				continue;
			File f = new File(attemptPath + name);
			try {
				f.getParentFile().mkdirs();
				f.createNewFile();

				FileWriter stream = new FileWriter(f);
				BufferedWriter out = new BufferedWriter(stream);
				out.write(StringUtils.htmlspecialchars_decode(m.get(name)));
				out.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (pathToTestingH != null) {
			ArrayList<String> strings = FileUtils.readFile(pathToTestingH);
			String testingH = StringUtils.implode("\r\n", strings);
			FileUtils.createFile(attemptPath + "testing.h", testingH);
		}
	}
}